package com.owg.jigsaw.javafx;

import com.owg.jigsaw.WBlock;
import com.owg.jigsaw.WCell;
import com.owg.jigsaw.WSingleFieldContainer;
import com.owg.jigsaw.slots.Slot;

import javafx.beans.value.ChangeListener;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.layout.Pane;
import owg.util.data.PFieldList;
import owg.util.data.ReferenceSet;
import owg.util.data.Referencer;

public class FXCell extends Pane
{
	protected class LayoutInfo
	{
		protected boolean isLayouting = false;
		protected boolean ignoreContentResize = false;
		
		protected int x;
		protected int y;
		protected int prefW;
		protected int prefH;
		protected int subWidth;
		protected int subHeight;
		protected Integer assignedW;
		protected Integer assignedH;
		
		//content isn't one of our classes so we need to store information about its layout
		//(it can't be recovered otherwise)
		protected int contentX;
		protected int contentY;
		protected int contentWidth;
		protected int contentHeight;
		public LayoutInfo()
		{
			x = (int) layoutXProperty().get();
			y = (int) layoutYProperty().get();
			recompute(null, null);
		}
		
		void recompute(Integer assignedW, Integer assignedH)
		{
			if(isLayouting)
				new Throwable("Warning: Attempt to recompute while layout is already in progress: ").printStackTrace();
			else
			{
				isLayouting = true;
				
				this.assignedW = assignedW;
				this.assignedH = assignedH;
				
				boolean firstX = true;
				int southSlotsHeight = 0;
				int southBlocksHeight = 0;
				int southBlocksWidth = 0;
				int southSlotsWidth = 0;
				for(FXSlot s : southSlots)
				{
					//The slot position is placed so that blocks can be added to it without colliding with any previous slots
					//But the slots width is created so that the content area can overlap with the horizontal space of previous blocks.
					southSlotsWidth = southBlocksWidth+FXBlock.TAB_OFFSET+s.getSlotWidth();
					southBlocksWidth += s.layout.prefW;
					southSlotsHeight = Math.max(southSlotsHeight, s.getSlotHeight());
					southBlocksHeight = Math.max(southBlocksHeight, s.layout.prefH-s.getSlotHeight());
					firstX = false;
				}

				int contentSpacing = 0;
				Group cp = model.contentPane.get();
				if(cp != null)
				{
					if(!firstX)
						contentSpacing += FXBlock.COMPONENT_SPACING;
					contentX = FXBlock.INDENTATION_OFFSET+southSlotsWidth+contentSpacing;
					contentY = FXBlock.INDENTATION_OFFSET;
					Bounds bds = cp.boundsInLocalProperty().get();
					contentWidth = (int) bds.getWidth();
					contentHeight = (int) bds.getHeight();
					firstX = false;
				}
				else
				{
					contentX = 0;
					contentY = 0;
					contentWidth = 0;
					contentHeight = 0;
				}
				
				int eastSlotsWidth = 0;
				int eastBlocksWidth = 0;
				int eastBlocksHeight = 0;
				for(FXSlot s : eastSlots)
				{
					eastBlocksHeight += s.layout.prefH;
					eastSlotsWidth = Math.max(eastSlotsWidth, s.getSlotWidth());
					eastBlocksWidth = Math.max(eastBlocksWidth, s.layout.prefW-s.getSlotWidth());
				}
				
				if(eastSlots.length > 0)
				{
					if(!firstX)
						contentSpacing += FXBlock.COMPONENT_SPACING;
					firstX = false;
				}
				//Note: This formula is used to expand the content area. Take special care if it needs to be changed.
				int midSubWidth = southSlotsWidth+contentSpacing+contentWidth+eastSlotsWidth;
				subWidth = FXBlock.INDENTATION_OFFSET + Math.max(southBlocksWidth, midSubWidth) + FXBlock.INDENTATION_OFFSET;
				subHeight = FXBlock.INDENTATION_OFFSET + Math.max(southSlotsHeight, Math.max(contentHeight, eastBlocksHeight)) + FXBlock.INDENTATION_OFFSET;
				
				//The upper-left cell must be large enough to hold the tabs, regardless of whether they are visible.
				if(model.x == 0 && model.y == 0)
				{
					subWidth = Math.max(subWidth, model.getBlock().tabType().get().getContour().getWidth()+2*FXBlock.TAB_OFFSET);
					subHeight = Math.max(subHeight, model.getBlock().tabType().get().getContour().getHeight()+2*FXBlock.TAB_OFFSET);
				}
				prefW = subWidth+eastBlocksWidth;
				prefH = subHeight+southBlocksHeight;
				
				if(southSlots.length > 0)
				{
					//Memo: The indentation offset cannot be filled with blocks, do it is not part of free space
					int freeWidth = getSubWidth()-southBlocksWidth-FXBlock.INDENTATION_OFFSET;
					//Also, if free space is allocated to any slots but the rightmost one, then they might overlap with the content area. We do not want this.
					int xOffset = 0;
					
					for (int sx = 0; sx < southSlots.length; sx++)
					{
						int w = southSlots[sx].layout.prefW;
						if(sx == 0)
						{
							if(southSlots[sx].isListConnectorSlot)
							{
								xOffset = 0;
								w += FXBlock.INDENTATION_OFFSET;
							}
							else
								xOffset = FXBlock.INDENTATION_OFFSET;
						}
						southSlots[sx].setSlotLocation(xOffset, yOfSouthSlot(sx));
						//Last one receives all space
						if(sx == southSlots.length-1)
							w += freeWidth;
						xOffset += w;
						southSlots[sx].setSlotSize(w, southSlots[sx].getSlotHeight()+southBlocksHeight);
					}
				}
				if(eastSlots.length > 0)
				{
					//Memo: The indentation offset cannot be filled with blocks, do it is not part of free space
					int freeHeight = getSubHeight()-eastBlocksHeight-FXBlock.INDENTATION_OFFSET;
					//For the east slots, space can be evenly allocated without risk of overlap with the content area:
					int incrHeight = freeHeight/eastSlots.length;
					int yOffset = 0;
					
					for (int sy = 0; sy < eastSlots.length; sy++)
					{
						int h = eastSlots[sy].layout.prefH+incrHeight;
						if(sy == 0)
						{
							if(eastSlots[sy].isListConnectorSlot)
							{
								yOffset = 0;
								h += FXBlock.INDENTATION_OFFSET;
							}
							else
								yOffset = FXBlock.INDENTATION_OFFSET;
						}
						eastSlots[sy].setSlotLocation(xOfEastSlot(sy), yOffset);
						freeHeight -= incrHeight;
						//Last one receives rounding error
						if(sy == eastSlots.length-1)
							h += freeHeight;
						yOffset += h;
						eastSlots[sy].setSlotSize(eastSlots[sy].getSlotWidth()+eastBlocksWidth, h);
					}
				}
				
				if(cp != null)
				{
					contentHeight = getSubHeight()-2*FXBlock.INDENTATION_OFFSET;
					contentWidth = getSubWidth()-southSlotsWidth+contentSpacing+eastSlotsWidth;
					ignoreContentResize = true;
					cp.resizeRelocate(contentX, contentY, contentWidth, contentHeight);
					ignoreContentResize = false;
					//cp.relocate(contentX, contentY);
				}
				
				isLayouting = false;
				
				//The assigned size will be non-null iff the layout call comes from the parent in the layout tree.
				//So, if it was null, notify the parent so it can update the size.
				if(assignedW == null)
					layoutListeners.fireEvents();
				requestLayout();
			}
		}

		private int yOfSouthSlot(int sx)
		{
			if(hasListConnectorSlot(model.getBlock().blockContainer().get(), false, model) && sx == 0)
				return getHeight()-southSlots[sx].getSlotHeight();
			else
				return getSubHeight()-southSlots[sx].getSlotHeight();
		}
		private int xOfEastSlot(int sy)
		{
			if(hasListConnectorSlot(model.getBlock().blockContainer().get(), true, model) && sy == 0)
				return getWidth()-eastSlots[sy].getSlotWidth();
			else
				return getSubWidth()-eastSlots[sy].getSlotWidth();
		}

		/**Returns the assigned width, or the preferred width if none has been assigned.*/
		public int getWidth()
		{
			if(assignedW == null)
				return prefW;
			return assignedW;
		}
		/**Returns the assigned height, or the preferred height if none has been assigned.*/
		public int getHeight()
		{
			if(assignedH == null)
				return prefH;
			return assignedH;
		}
		
		/**Returns the assigned subwidth, or the preferred subwidth if none has been assigned.*/
		public int getSubWidth()
		{
			if(assignedW == null)
				return subWidth;
			return assignedW-(prefW-subWidth);
		}
		/**Returns the assigned subheight, or the preferred subheight if none has been assigned.*/
		public int getSubHeight()
		{
			if(assignedH == null)
				return subHeight;
			return assignedH-(prefH-subHeight);
		}
	}
	final ActionSource layoutListeners = new ActionSource();
	protected LayoutInfo layout;
	protected boolean authorizeLocChange = false;

	public final WCell model;
	
	FXSlot[] southSlots;
	FXSlot[] eastSlots;
	//FXColoredImageView[] southTabs;
	//FXColoredImageView[] eastTabs;
	
	private final Referencer referencer = new ReferenceSet();
	private final ChangeListener<Bounds> contentPaneBoundsListener;
	protected final EventHandler<ActionEvent> layoutListener;

	public FXCell(WCell model)
	{
		super();
		//JavaFX's layout handling is terrible and out of order, 
		//meaning we need to manually synchronize nested layout panes.
		//To avoid this as much as possible, avoid nesting of layout panes.
		//Instead, write the entire layout using as few classes as possible.
		this.model = model;
		
		//Create a reusable listener for the layout of slots
		layoutListener = ((e) -> {layout.recompute(null, null);});
		
		//Create a reusable listener for the content pane bounds
		contentPaneBoundsListener = ((e, o, n) -> {if(!layout.ignoreContentResize) layout.recompute(null, null);});
		//Add the content pane
		Group cp = model.contentPane.get();
		if(cp != null)
		{
			getChildren().add(cp);
			cp.boundsInLocalProperty().addListener(contentPaneBoundsListener);
		}
		//Make sure the content pane is properly updated if it changes, 'moving' the listener to the new pane
		model.contentPane.addListener(referencer, (prop, oldVal, newVal) -> 
		{
			if(oldVal != null)
			{
				oldVal.boundsInLocalProperty().removeListener(contentPaneBoundsListener);
				getChildren().remove(oldVal);
			}
			if(newVal != null)
			{
				getChildren().add(newVal);
				newVal.boundsInLocalProperty().addListener(contentPaneBoundsListener);
			}
			layout.recompute(null, null);
		});
		southSlots = buildSlots(model.southSlots, false);
		model.southSlots.addListener(referencer, (prop, index, oldVal, newVal) ->
		{
			//Cleanup
			removeSlots(southSlots);
			//Rebuild
			southSlots = buildSlots(model.southSlots, false);
			if(layout != null)
				layout.recompute(null, null);
		});
		eastSlots = buildSlots(model.eastSlots, true);
		model.eastSlots.addListener(referencer, (prop, index, oldVal, newVal) ->
		{
			//Cleanup
			removeSlots(eastSlots);
			//Rebuild
			eastSlots = buildSlots(model.eastSlots, true);
			if(layout != null)
				layout.recompute(null, null);
		});
		
		model.getBlock().blockContainer().addListener(referencer, (prop, oldVal, newVal) ->
		{	
			if(hasListConnectorSlot(oldVal, false, model) != hasListConnectorSlot(newVal, false, model))
			{
				//Vertical slot changed
				//Cleanup
				removeSlots(southSlots);
				//Rebuild
				southSlots = buildSlots(model.southSlots, false);
				if(layout != null)
					layout.recompute(null, null);
			}
			if(hasListConnectorSlot(oldVal, true, model) != hasListConnectorSlot(newVal, true, model))
			{
				//Horizontal slot changed
				//Cleanup
				removeSlots(eastSlots);
				//Rebuild
				eastSlots = buildSlots(model.eastSlots, true);
				if(layout != null)
					layout.recompute(null, null);
			}
		});
		
		layoutXProperty().addListener(e -> 
		{
			if(!authorizeLocChange)
				throw new IllegalStateException("Unauthorized layout x change for: "+this+" (FXCell can only be a part of FXBlock)");
			assert layout.x == (int) layoutXProperty().get();
		});
		layoutYProperty().addListener(e -> 
		{
			if(!authorizeLocChange)
				throw new IllegalStateException("Unauthorized layout y change for: "+this+" (FXCell can only be a part of FXBlock)");
			assert layout.y == (int) layoutYProperty().get();
		});
		
		layout = new LayoutInfo();
	}
	private void removeSlots(FXSlot[] slots)
	{
		for(FXSlot s : slots)
		{
			//Ensure all listeners are removed since the FXSlot instances are usually reused
			s.layoutListeners.removeActionListener(layoutListener);
			getChildren().remove(s);
		}
	}
	static boolean hasListConnectorSlot(WSingleFieldContainer<WBlock> container, boolean horizontalDirection, WCell model)
	{
		if(container == null)
			return false;
		return model.x == (horizontalDirection?(model.getBlock().getNumColumnsAt(0)-1):0) && 
			model.y == (horizontalDirection?0:(model.getBlock().getNumRows()-1)) &&
			hasListConnectorSlot(container, horizontalDirection);
	}
	static boolean hasListConnectorSlot(WSingleFieldContainer<WBlock> container, boolean horizontalDirection)
	{
		return(container instanceof Slot && //Parent block is docked to a slot
			(((Slot)container).isHorizontal() == horizontalDirection) && //Docked slot has same orientation as this slot list
			((Slot)container).isMultiSlot); //Docked slot is a multi-slot
	}
	
	private FXSlot[] buildSlots(PFieldList<Slot> slots, boolean horizontalSlots)
	{
		boolean hasListConnectorSlot = hasListConnectorSlot(model.getBlock().blockContainer().get(), horizontalSlots, model);
		
		FXSlot[] r = new FXSlot[slots.size()+(hasListConnectorSlot?1:0)];
		
		int sx = 0;
		if(hasListConnectorSlot)
		{
			r[sx] = new FXSlot((Slot)model.block.getParent(), model.block.color(), true);
			r[sx].layoutListeners.addActionListener(layoutListener);
			getChildren().add(r[sx]);
			sx++;
		}
		for(Slot s : slots)
		{
			r[sx] = (FXSlot) s.getUI();
			getChildren().add(r[sx]);
			r[sx].layoutListeners.addActionListener(layoutListener);
			sx++;
		}
		return r;
	}
	/**Returns the x coordinate of the cell.*/
	public int getRealX()
	{
		return layout.x;
	}
	/**Returns the y coordinate of the cell.*/
	public int getRealY()
	{
		return layout.y;
	}
	/**Returns the width of the cell.*/
	public int getRealWidth()
	{
		return layout.getWidth();
	}
	/**Returns the height of the cell.*/
	public int getRealHeight()
	{
		return layout.getHeight();
	}
	/**Returns the width of the cell excluding attached blocks.*/
	public int getSubWidth()
	{
		return layout.getSubWidth();
	}
	/**Returns the height of the cell excluding attached blocks.*/
	public int getSubHeight()
	{
		return layout.getSubHeight();
	}
	
	public void setCellX(int x)
	{
		authorizeLocChange = true;
		layout.x = x;
		layoutXProperty().set(x);
		authorizeLocChange = false;
	}public void setCellY(int y)
	{
		authorizeLocChange = true;
		layout.y = y;
		layoutYProperty().set(y);
		authorizeLocChange = false;
	}
	public void setCellLocation(int x, int y)
	{
		authorizeLocChange = true;
		layout.x = x;
		layout.y = y;
		layoutXProperty().set(x);
		layoutYProperty().set(y);
		authorizeLocChange = false;
	}
	public void setCellSize(int w, int h)
	{
		if(w < layout.prefW)
			new Throwable(this+": Warning: Assigned width("+w+") is less than preferred width("+layout.prefW+")!").printStackTrace();
		if(h < layout.prefH)
			new Throwable(this+": Warning: Assigned height("+h+") is less than preferred height("+layout.prefH+")!").printStackTrace();
		layout.recompute(w, h);
	}
	
	@Override
	protected void layoutChildren()
	{
		//The layout system is completely fucked and can't be used
		//It's like they couldn't fathom that someone would want to make nested layouts
		//There's no way to communicate between panes unless you wait for the next tick, creating graphical glitches
	}
	public WCell getModel()
	{
		return model;
	}
}
